{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Quickstart\n",
    "\n",
    "Tutorial for [NautilusTrader](https://nautilustrader.io/docs/) a high-performance algorithmic trading platform and event driven backtester.\n",
    "\n",
    "[View source on GitHub](https://github.com/nautechsystems/nautilus_trader/blob/develop/docs/getting_started/quickstart.ipynb)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1",
   "metadata": {},
   "source": [
    "## Overview\n",
    "\n",
    "This quickstart tutorial shows you how to get up and running with NautilusTrader backtesting using FX data.\n",
    "To support this, we provide pre-loaded test data in the standard Nautilus persistence format (Parquet).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "- Python 3.11+ installed.\n",
    "- [NautilusTrader](https://pypi.org/project/nautilus_trader/) latest release installed (`pip install -U nautilus_trader`).\n",
    "- [JupyterLab](https://jupyter.org/) or similar installed (`pip install -U jupyterlab`)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3",
   "metadata": {},
   "source": [
    "## 1. Get sample data\n",
    "\n",
    "To save time, we have prepared sample data in the Nautilus format for use with this example. \n",
    "Run the next cell to download and set up the data (this should take ~ 1-2 mins).\n",
    "\n",
    "For further details on how to load data into Nautilus, see [Loading External Data](https://nautilustrader.io/docs/latest/concepts/data#loading-data) guide."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import urllib.request\n",
    "from pathlib import Path\n",
    "\n",
    "from nautilus_trader.persistence.catalog import ParquetDataCatalog\n",
    "from nautilus_trader.persistence.wranglers import QuoteTickDataWrangler\n",
    "from nautilus_trader.test_kit.providers import CSVTickDataLoader\n",
    "from nautilus_trader.test_kit.providers import TestInstrumentProvider\n",
    "\n",
    "\n",
    "# Change to project root directory\n",
    "original_cwd = os.getcwd()\n",
    "project_root = os.path.abspath(os.path.join(os.getcwd(), \"..\", \"..\"))\n",
    "os.chdir(project_root)\n",
    "\n",
    "print(f\"Working directory: {os.getcwd()}\")\n",
    "\n",
    "# Create catalog directory\n",
    "catalog_path = Path(\"catalog\")\n",
    "catalog_path.mkdir(exist_ok=True)\n",
    "\n",
    "print(f\"Catalog directory: {catalog_path.absolute()}\")\n",
    "\n",
    "try:\n",
    "    # Download EUR/USD sample data\n",
    "    print(\"Downloading EUR/USD sample data...\")\n",
    "    url = \"https://raw.githubusercontent.com/nautechsystems/nautilus_data/main/raw_data/fx_hist_data/DAT_ASCII_EURUSD_T_202001.csv.gz\"\n",
    "    filename = \"EURUSD_202001.csv.gz\"\n",
    "\n",
    "    print(f\"Downloading from: {url}\")\n",
    "    urllib.request.urlretrieve(url, filename)  # noqa: S310\n",
    "    print(\"Download complete\")\n",
    "\n",
    "    # Create the instrument using the current schema (includes multiplier)\n",
    "    print(\"Creating EUR/USD instrument...\")\n",
    "    instrument = TestInstrumentProvider.default_fx_ccy(\"EUR/USD\")\n",
    "\n",
    "    # Load and process the tick data\n",
    "    print(\"Loading tick data...\")\n",
    "    wrangler = QuoteTickDataWrangler(instrument)\n",
    "\n",
    "    df = CSVTickDataLoader.load(\n",
    "        filename,\n",
    "        index_col=0,\n",
    "        datetime_format=\"%Y%m%d %H%M%S%f\",\n",
    "    )\n",
    "    df.columns = [\"bid_price\", \"ask_price\", \"size\"]\n",
    "    print(f\"Loaded {len(df)} ticks\")\n",
    "\n",
    "    # Process ticks\n",
    "    print(\"Processing ticks...\")\n",
    "    ticks = wrangler.process(df)\n",
    "\n",
    "    # Write to catalog\n",
    "    print(\"Writing data to catalog...\")\n",
    "    catalog = ParquetDataCatalog(str(catalog_path))\n",
    "\n",
    "    # Write instrument first\n",
    "    catalog.write_data([instrument])\n",
    "    print(\"Instrument written to catalog\")\n",
    "\n",
    "    # Write tick data\n",
    "    catalog.write_data(ticks)\n",
    "    print(\"Tick data written to catalog\")\n",
    "\n",
    "    # Verify what was written\n",
    "    print(\"\\nVerifying catalog contents...\")\n",
    "    test_catalog = ParquetDataCatalog(str(catalog_path))\n",
    "    loaded_instruments = test_catalog.instruments()\n",
    "    print(f\"Instruments in catalog: {[str(i.id) for i in loaded_instruments]}\")\n",
    "\n",
    "    # Clean up downloaded file\n",
    "    os.unlink(filename)\n",
    "    print(\"\\nData setup complete!\")\n",
    "\n",
    "except Exception as e:\n",
    "    print(f\"Error: {e}\")\n",
    "    import traceback\n",
    "    traceback.print_exc()\n",
    "finally:\n",
    "    os.chdir(original_cwd)\n",
    "    print(f\"Changed back to: {os.getcwd()}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "from nautilus_trader.backtest.node import BacktestDataConfig\n",
    "from nautilus_trader.backtest.node import BacktestEngineConfig\n",
    "from nautilus_trader.backtest.node import BacktestNode\n",
    "from nautilus_trader.backtest.node import BacktestRunConfig\n",
    "from nautilus_trader.backtest.node import BacktestVenueConfig\n",
    "from nautilus_trader.config import ImportableStrategyConfig\n",
    "from nautilus_trader.config import LoggingConfig\n",
    "from nautilus_trader.model import Quantity\n",
    "from nautilus_trader.model import QuoteTick\n",
    "from nautilus_trader.persistence.catalog import ParquetDataCatalog"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6",
   "metadata": {},
   "source": [
    "## 2. Set up a Parquet data catalog\n",
    "\n",
    "If everything worked correctly, you should be able to see a single EUR/USD instrument in the catalog."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Load the catalog from the project root directory\n",
    "project_root = os.path.abspath(os.path.join(os.getcwd(), \"..\", \"..\"))\n",
    "catalog_path = os.path.join(project_root, \"catalog\")\n",
    "\n",
    "catalog = ParquetDataCatalog(catalog_path)\n",
    "instruments = catalog.instruments()\n",
    "\n",
    "print(f\"Loaded catalog from: {catalog_path}\")\n",
    "print(f\"Available instruments: {[str(i.id) for i in instruments]}\")\n",
    "\n",
    "if instruments:\n",
    "    print(f\"\\nUsing instrument: {instruments[0].id}\")\n",
    "else:\n",
    "    print(\"\\nNo instruments found. Please run the data download cell first.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8",
   "metadata": {},
   "source": [
    "## 3. Write a trading strategy\n",
    "\n",
    "NautilusTrader includes many built-in indicators. In this example we use the MACD indicator to build a simple trading strategy.\n",
    "\n",
    "You can read more about [MACD here](https://www.investopedia.com/terms/m/macd.asp); this indicator merely serves as an example without any expected alpha. You can also register indicators to receive certain data types; however, in this example we manually pass the received `QuoteTick` to the indicator in the `on_quote_tick` method.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nautilus_trader.core.message import Event\n",
    "from nautilus_trader.indicators import MovingAverageConvergenceDivergence\n",
    "from nautilus_trader.model import InstrumentId\n",
    "from nautilus_trader.model import Position\n",
    "from nautilus_trader.model.enums import OrderSide\n",
    "from nautilus_trader.model.enums import PositionSide\n",
    "from nautilus_trader.model.enums import PriceType\n",
    "from nautilus_trader.model.events import PositionClosed\n",
    "from nautilus_trader.model.events import PositionOpened\n",
    "from nautilus_trader.trading.strategy import Strategy\n",
    "from nautilus_trader.trading.strategy import StrategyConfig\n",
    "\n",
    "\n",
    "class MACDConfig(StrategyConfig):\n",
    "    instrument_id: InstrumentId\n",
    "    fast_period: int = 12\n",
    "    slow_period: int = 26\n",
    "    trade_size: int = 1_000_000\n",
    "\n",
    "\n",
    "class MACDStrategy(Strategy):\n",
    "    \"\"\"A MACD-based strategy that only trades on zero-line crossovers.\"\"\"\n",
    "\n",
    "    def __init__(self, config: MACDConfig):\n",
    "        super().__init__(config=config)\n",
    "        # Our \"trading signal\"\n",
    "        self.macd = MovingAverageConvergenceDivergence(\n",
    "            fast_period=config.fast_period, slow_period=config.slow_period, price_type=PriceType.MID\n",
    "        )\n",
    "\n",
    "        self.trade_size = Quantity.from_int(config.trade_size)\n",
    "\n",
    "        # Track our position and MACD state\n",
    "        self.position: Position | None = None\n",
    "        self.last_macd_above_zero = None  # Track if MACD was above zero on last check\n",
    "\n",
    "    def on_start(self):\n",
    "        \"\"\"Subscribe to market data on strategy start.\"\"\"\n",
    "        self.subscribe_quote_ticks(instrument_id=self.config.instrument_id)\n",
    "\n",
    "    def on_stop(self):\n",
    "        \"\"\"Clean up on strategy stop.\"\"\"\n",
    "        self.close_all_positions(self.config.instrument_id)\n",
    "        self.unsubscribe_quote_ticks(instrument_id=self.config.instrument_id)\n",
    "\n",
    "    def on_quote_tick(self, tick: QuoteTick):\n",
    "        \"\"\"Process incoming quote ticks.\"\"\"\n",
    "        # Update indicator\n",
    "        self.macd.handle_quote_tick(tick)\n",
    "\n",
    "        if not self.macd.initialized:\n",
    "            return  # Wait for indicator to warm up\n",
    "\n",
    "        # Check for trading opportunities\n",
    "        self.check_signals()\n",
    "\n",
    "    def on_event(self, event: Event):\n",
    "        \"\"\"Handle position events.\"\"\"\n",
    "        if isinstance(event, PositionOpened):\n",
    "            self.position = self.cache.position(event.position_id)\n",
    "            self._log.info(f\"Position opened: {self.position.side} @ {self.position.avg_px_open}\")\n",
    "        elif isinstance(event, PositionClosed):\n",
    "            if self.position and self.position.id == event.position_id:\n",
    "                self._log.info(f\"Position closed with PnL: {self.position.realized_pnl}\")\n",
    "                self.position = None\n",
    "\n",
    "    def check_signals(self):\n",
    "        \"\"\"Check MACD signals - only act on actual crossovers.\"\"\"\n",
    "        current_macd = self.macd.value\n",
    "        current_above_zero = current_macd > 0\n",
    "\n",
    "        # Skip if this is the first reading\n",
    "        if self.last_macd_above_zero is None:\n",
    "            self.last_macd_above_zero = current_above_zero\n",
    "            return\n",
    "\n",
    "        # Only act on actual crossovers\n",
    "        if self.last_macd_above_zero != current_above_zero:\n",
    "            if current_above_zero:  # Just crossed above zero\n",
    "                # Only go long if we're not already long\n",
    "                if not self.is_long:\n",
    "                    # Close any short position first\n",
    "                    if self.is_short:\n",
    "                        self.close_position(self.position)\n",
    "                    # Then go long (but only when flat)\n",
    "                    self.go_long()\n",
    "\n",
    "            else:  # Just crossed below zero\n",
    "                # Only go short if we're not already short\n",
    "                if not self.is_short:\n",
    "                    # Close any long position first\n",
    "                    if self.is_long:\n",
    "                        self.close_position(self.position)\n",
    "                    # Then go short (but only when flat)\n",
    "                    self.go_short()\n",
    "\n",
    "        self.last_macd_above_zero = current_above_zero\n",
    "\n",
    "    def go_long(self):\n",
    "        \"\"\"Enter long position only if flat.\"\"\"\n",
    "        if self.is_flat:\n",
    "            order = self.order_factory.market(\n",
    "                instrument_id=self.config.instrument_id,\n",
    "                order_side=OrderSide.BUY,\n",
    "                quantity=self.trade_size,\n",
    "            )\n",
    "            self.submit_order(order)\n",
    "            self._log.info(f\"Going LONG - MACD crossed above zero: {self.macd.value:.6f}\")\n",
    "\n",
    "    def go_short(self):\n",
    "        \"\"\"Enter short position only if flat.\"\"\"\n",
    "        if self.is_flat:\n",
    "            order = self.order_factory.market(\n",
    "                instrument_id=self.config.instrument_id,\n",
    "                order_side=OrderSide.SELL,\n",
    "                quantity=self.trade_size,\n",
    "            )\n",
    "            self.submit_order(order)\n",
    "            self._log.info(f\"Going SHORT - MACD crossed below zero: {self.macd.value:.6f}\")\n",
    "\n",
    "    @property\n",
    "    def is_flat(self) -> bool:\n",
    "        \"\"\"Check if we have no position.\"\"\"\n",
    "        return self.position is None\n",
    "\n",
    "    @property\n",
    "    def is_long(self) -> bool:\n",
    "        \"\"\"Check if we have a long position.\"\"\"\n",
    "        return self.position and self.position.side == PositionSide.LONG\n",
    "\n",
    "    @property\n",
    "    def is_short(self) -> bool:\n",
    "        \"\"\"Check if we have a short position.\"\"\"\n",
    "        return self.position and self.position.side == PositionSide.SHORT\n",
    "\n",
    "    def on_dispose(self):\n",
    "        \"\"\"Clean up on strategy disposal.\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10",
   "metadata": {},
   "source": [
    "### Enhanced Strategy with Stop-Loss and Take-Profit\n",
    "\n",
    "The basic MACD strategy above will now generate trades. For better risk management, here's an enhanced version with stop-loss and take-profit orders:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "11",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nautilus_trader.model.objects import Price\n",
    "\n",
    "\n",
    "class MACDEnhancedConfig(StrategyConfig):\n",
    "    instrument_id: InstrumentId\n",
    "    fast_period: int = 12\n",
    "    slow_period: int = 26\n",
    "    trade_size: int = 1_000_000\n",
    "    entry_threshold: float = 0.00005\n",
    "    exit_threshold: float = 0.00002\n",
    "    stop_loss_pips: int = 20  # Stop loss in pips\n",
    "    take_profit_pips: int = 40  # Take profit in pips\n",
    "\n",
    "\n",
    "class MACDEnhancedStrategy(Strategy):\n",
    "    \"\"\"Enhanced MACD strategy with stop-loss and take-profit.\"\"\"\n",
    "\n",
    "    def __init__(self, config: MACDEnhancedConfig):\n",
    "        super().__init__(config=config)\n",
    "        self.macd = MovingAverageConvergenceDivergence(\n",
    "            fast_period=config.fast_period, slow_period=config.slow_period, price_type=PriceType.MID\n",
    "        )\n",
    "\n",
    "        self.trade_size = Quantity.from_int(config.trade_size)\n",
    "        self.position: Position | None = None\n",
    "        self.last_macd_sign = 0\n",
    "\n",
    "    def on_start(self):\n",
    "        \"\"\"Subscribe to market data on strategy start.\"\"\"\n",
    "        self.subscribe_quote_ticks(instrument_id=self.config.instrument_id)\n",
    "\n",
    "    def on_stop(self):\n",
    "        \"\"\"Clean up on strategy stop.\"\"\"\n",
    "        self.cancel_all_orders(self.config.instrument_id)\n",
    "        self.close_all_positions(self.config.instrument_id)\n",
    "        self.unsubscribe_quote_ticks(instrument_id=self.config.instrument_id)\n",
    "\n",
    "    def on_quote_tick(self, tick: QuoteTick):\n",
    "        \"\"\"Process incoming quote ticks.\"\"\"\n",
    "        self.macd.handle_quote_tick(tick)\n",
    "\n",
    "        if not self.macd.initialized:\n",
    "            return\n",
    "\n",
    "        self.check_signals(tick)\n",
    "\n",
    "    def on_event(self, event: Event):\n",
    "        \"\"\"Handle position events.\"\"\"\n",
    "        if isinstance(event, PositionOpened):\n",
    "            self.position = self.cache.position(event.position_id)\n",
    "            self._log.info(f\"Position opened: {self.position.side} @ {self.position.avg_px_open}\")\n",
    "            # Place stop-loss and take-profit orders\n",
    "            self.place_exit_orders()\n",
    "        elif isinstance(event, PositionClosed):\n",
    "            if self.position and self.position.id == event.position_id:\n",
    "                pnl = self.position.realized_pnl\n",
    "                self._log.info(f\"Position closed with PnL: {pnl}\")\n",
    "                self.position = None\n",
    "                # Cancel any remaining exit orders\n",
    "                self.cancel_all_orders(self.config.instrument_id)\n",
    "\n",
    "    def check_signals(self, tick: QuoteTick):\n",
    "        \"\"\"Check MACD signals and manage positions.\"\"\"\n",
    "        current_macd = self.macd.value\n",
    "        current_sign = 1 if current_macd > 0 else -1\n",
    "\n",
    "        # Skip if we already have a position\n",
    "        if self.position:\n",
    "            return\n",
    "\n",
    "        # Detect MACD zero-line crossover\n",
    "        if self.last_macd_sign != 0 and self.last_macd_sign != current_sign:\n",
    "            if current_sign > 0:\n",
    "                self.go_long(tick)\n",
    "            else:\n",
    "                self.go_short(tick)\n",
    "\n",
    "        # Entry signals based on threshold\n",
    "        elif abs(current_macd) > self.config.entry_threshold:\n",
    "            if current_macd > self.config.entry_threshold:\n",
    "                self.go_long(tick)\n",
    "            elif current_macd < -self.config.entry_threshold:\n",
    "                self.go_short(tick)\n",
    "\n",
    "        self.last_macd_sign = current_sign\n",
    "\n",
    "    def go_long(self, tick: QuoteTick):\n",
    "        \"\"\"Enter long position.\"\"\"\n",
    "        if self.position:\n",
    "            return  # Already have a position\n",
    "\n",
    "        order = self.order_factory.market(\n",
    "            instrument_id=self.config.instrument_id,\n",
    "            order_side=OrderSide.BUY,\n",
    "            quantity=self.trade_size,\n",
    "        )\n",
    "        self.submit_order(order)\n",
    "        self._log.info(f\"Going LONG @ {tick.ask_price} - MACD: {self.macd.value:.6f}\")\n",
    "\n",
    "    def go_short(self, tick: QuoteTick):\n",
    "        \"\"\"Enter short position.\"\"\"\n",
    "        if self.position:\n",
    "            return  # Already have a position\n",
    "\n",
    "        order = self.order_factory.market(\n",
    "            instrument_id=self.config.instrument_id,\n",
    "            order_side=OrderSide.SELL,\n",
    "            quantity=self.trade_size,\n",
    "        )\n",
    "        self.submit_order(order)\n",
    "        self._log.info(f\"Going SHORT @ {tick.bid_price} - MACD: {self.macd.value:.6f}\")\n",
    "\n",
    "    def place_exit_orders(self):\n",
    "        \"\"\"Place stop-loss and take-profit orders for the current position.\"\"\"\n",
    "        if not self.position:\n",
    "            return\n",
    "\n",
    "        entry_price = float(self.position.avg_px_open)\n",
    "        pip_value = 0.0001  # For FX pairs (adjust for different instruments)\n",
    "\n",
    "        if self.position.side == PositionSide.LONG:\n",
    "            # Long position: stop below entry, target above\n",
    "            stop_price = entry_price - (self.config.stop_loss_pips * pip_value)\n",
    "            target_price = entry_price + (self.config.take_profit_pips * pip_value)\n",
    "\n",
    "            # Stop-loss order\n",
    "            stop_loss = self.order_factory.stop_market(\n",
    "                instrument_id=self.config.instrument_id,\n",
    "                order_side=OrderSide.SELL,\n",
    "                quantity=self.trade_size,\n",
    "                trigger_price=Price.from_str(f\"{stop_price:.5f}\"),\n",
    "            )\n",
    "            self.submit_order(stop_loss)\n",
    "\n",
    "            # Take-profit order\n",
    "            take_profit = self.order_factory.limit(\n",
    "                instrument_id=self.config.instrument_id,\n",
    "                order_side=OrderSide.SELL,\n",
    "                quantity=self.trade_size,\n",
    "                price=Price.from_str(f\"{target_price:.5f}\"),\n",
    "            )\n",
    "            self.submit_order(take_profit)\n",
    "\n",
    "            self._log.info(f\"Placed LONG exit orders - Stop: {stop_price:.5f}, Target: {target_price:.5f}\")\n",
    "\n",
    "        else:  # SHORT position\n",
    "            # Short position: stop above entry, target below\n",
    "            stop_price = entry_price + (self.config.stop_loss_pips * pip_value)\n",
    "            target_price = entry_price - (self.config.take_profit_pips * pip_value)\n",
    "\n",
    "            # Stop-loss order\n",
    "            stop_loss = self.order_factory.stop_market(\n",
    "                instrument_id=self.config.instrument_id,\n",
    "                order_side=OrderSide.BUY,\n",
    "                quantity=self.trade_size,\n",
    "                trigger_price=Price.from_str(f\"{stop_price:.5f}\"),\n",
    "            )\n",
    "            self.submit_order(stop_loss)\n",
    "\n",
    "            # Take-profit order\n",
    "            take_profit = self.order_factory.limit(\n",
    "                instrument_id=self.config.instrument_id,\n",
    "                order_side=OrderSide.BUY,\n",
    "                quantity=self.trade_size,\n",
    "                price=Price.from_str(f\"{target_price:.5f}\"),\n",
    "            )\n",
    "            self.submit_order(take_profit)\n",
    "\n",
    "            self._log.info(f\"Placed SHORT exit orders - Stop: {stop_price:.5f}, Target: {target_price:.5f}\")\n",
    "\n",
    "    def on_dispose(self):\n",
    "        \"\"\"Clean up on strategy disposal.\"\"\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12",
   "metadata": {},
   "source": [
    "## Configuring backtests\n",
    "\n",
    "Now that we have a trading strategy and data, we can begin to configure a backtest run. Nautilus uses a `BacktestNode` to orchestrate backtest runs, which requires some setup. This may seem a little complex at first, however this is necessary for the capabilities that Nautilus strives for.\n",
    "\n",
    "To configure a `BacktestNode`, we first need to create an instance of a `BacktestRunConfig`, configuring the following (minimal) aspects of the backtest:\n",
    "\n",
    "- `engine`: The engine for the backtest representing our core system, which will also contain our strategies.\n",
    "- `venues`: The simulated venues (exchanges or brokers) available in the backtest.\n",
    "- `data`: The input data we would like to perform the backtest on.\n",
    "\n",
    "There are many more configurable features described later in the docs; for now this will get us up and running.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "13",
   "metadata": {},
   "outputs": [],
   "source": [
    "venue = BacktestVenueConfig(\n",
    "    name=\"SIM\",\n",
    "    oms_type=\"NETTING\",\n",
    "    account_type=\"MARGIN\",\n",
    "    base_currency=\"USD\",\n",
    "    starting_balances=[\"1_000_000 USD\"]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "## 5. Configure data\n",
    "\n",
    "We need to know about the instruments that we would like to load data for, we can use the `ParquetDataCatalog` for this."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "instruments = catalog.instruments()\n",
    "instruments"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16",
   "metadata": {},
   "source": [
    "Next, configure the data for the backtest. Nautilus provides a flexible data-loading system for backtests, but that flexibility requires some configuration.\n",
    "\n",
    "For each tick type (and instrument), we add a `BacktestDataConfig`. In this instance we are simply adding the `QuoteTick`(s) for our EUR/USD instrument:\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nautilus_trader.model import QuoteTick\n",
    "\n",
    "\n",
    "data = BacktestDataConfig(\n",
    "    catalog_path=str(catalog.path),\n",
    "    data_cls=QuoteTick,\n",
    "    instrument_id=instruments[0].id,\n",
    "    end_time=\"2020-01-10\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "18",
   "metadata": {},
   "source": [
    "## 6. Configure engine\n",
    "\n",
    "Create a `BacktestEngineConfig` to represent the configuration of our core trading system.\n",
    "Pass in your trading strategies, adjust the log level as needed, and configure any other components (the defaults are fine too).\n",
    "\n",
    "Add strategies via the `ImportableStrategyConfig`, which enables importing strategies from arbitrary files or user packages. In this instance our `MACDStrategy` lives in the current module, which Python refers to as `__main__`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19",
   "metadata": {},
   "outputs": [],
   "source": [
    "# NautilusTrader currently exceeds the rate limit for Jupyter notebook logging (stdout output),\n",
    "# this is why the `log_level` is set to \"ERROR\". If you lower this level to see\n",
    "# more logging then the notebook will hang during cell execution. A fix is currently\n",
    "# being investigated which involves either raising the configured rate limits for\n",
    "# Jupyter, or throttling the log flushing from Nautilus.\n",
    "# https://github.com/jupyterlab/jupyterlab/issues/12845\n",
    "# https://github.com/deshaw/jupyterlab-limit-output\n",
    "engine = BacktestEngineConfig(\n",
    "    strategies=[\n",
    "        ImportableStrategyConfig(\n",
    "            strategy_path=\"__main__:MACDStrategy\",\n",
    "            config_path=\"__main__:MACDConfig\",\n",
    "            config={\n",
    "              \"instrument_id\": instruments[0].id,\n",
    "              \"fast_period\": 12,\n",
    "              \"slow_period\": 26,\n",
    "            },\n",
    "        )\n",
    "    ],\n",
    "    logging=LoggingConfig(log_level=\"ERROR\"),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20",
   "metadata": {},
   "source": [
    "## 7. Run backtest\n",
    "\n",
    "We can now pass our various config pieces to the `BacktestRunConfig`. This object now contains the \n",
    "full configuration for our backtest."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21",
   "metadata": {},
   "outputs": [],
   "source": [
    "config = BacktestRunConfig(\n",
    "    engine=engine,\n",
    "    venues=[venue],\n",
    "    data=[data],\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22",
   "metadata": {},
   "source": [
    "The `BacktestNode` class orchestrates the backtest run. This separation between configuration and execution enables the `BacktestNode` to run multiple configurations (different parameters or batches of data). We are now ready to run some backtests.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nautilus_trader.backtest.results import BacktestResult\n",
    "\n",
    "\n",
    "node = BacktestNode(configs=[config])\n",
    "\n",
    " # Runs one or many configs synchronously\n",
    "results: list[BacktestResult] = node.run()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24",
   "metadata": {},
   "source": [
    "### Expected Output\n",
    "\n",
    "When you run the backtest with the improved MACD strategy, you should see:\n",
    "- **Actual trades being executed** (both BUY and SELL orders).\n",
    "- **Positions being opened and closed** with proper exit logic.\n",
    "- **P&L calculations** showing wins and losses.\n",
    "- **Performance metrics** including win rate, profit factor, and additional statistics.\n",
    "\n",
    "If you're not seeing any trades, check:\n",
    "1. The data time range (you may need more data).\n",
    "2. The threshold parameters (they might be too restrictive).\n",
    "3. The indicator warm-up period (MACD needs time to initialize).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25",
   "metadata": {},
   "source": [
    "## 8. Analyze results"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26",
   "metadata": {},
   "source": [
    "Now that the run is complete, we can also directly query for the `BacktestEngine`(s) used internally by the `BacktestNode`\n",
    "by using the run configs ID. \n",
    "\n",
    "The engine(s) can provide additional reports and information."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27",
   "metadata": {},
   "outputs": [],
   "source": [
    "from nautilus_trader.backtest.engine import BacktestEngine\n",
    "from nautilus_trader.model import Venue\n",
    "\n",
    "\n",
    "engine: BacktestEngine = node.get_engine(config.id)\n",
    "\n",
    "len(engine.trader.generate_order_fills_report())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "28",
   "metadata": {},
   "outputs": [],
   "source": [
    "engine.trader.generate_positions_report()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29",
   "metadata": {},
   "outputs": [],
   "source": [
    "engine.trader.generate_account_report(Venue(\"SIM\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "30",
   "metadata": {},
   "source": [
    "## 9. Performance Metrics\n",
    "\n",
    "Let's add some additional performance metrics to better understand how our strategy performed:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "31",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get performance statistics\n",
    "\n",
    "# Get the account and positions\n",
    "account = engine.trader.generate_account_report(Venue(\"SIM\"))\n",
    "positions = engine.trader.generate_positions_report()\n",
    "orders = engine.trader.generate_order_fills_report()\n",
    "\n",
    "# Print summary statistics\n",
    "print(\"=== STRATEGY PERFORMANCE ===\")\n",
    "print(f\"Total Orders: {len(orders)}\")\n",
    "print(f\"Total Positions: {len(positions)}\")\n",
    "\n",
    "if len(positions) > 0:\n",
    "    # Convert P&L strings to numeric values\n",
    "    positions[\"pnl_numeric\"] = positions[\"realized_pnl\"].apply(\n",
    "        lambda x: float(str(x).replace(\" USD\", \"\").replace(\",\", \"\")) if isinstance(x, str) else float(x)\n",
    "    )\n",
    "\n",
    "    # Calculate win rate\n",
    "    winning_trades = positions[positions[\"pnl_numeric\"] > 0]\n",
    "    losing_trades = positions[positions[\"pnl_numeric\"] < 0]\n",
    "\n",
    "    win_rate = len(winning_trades) / len(positions) * 100 if len(positions) > 0 else 0\n",
    "\n",
    "    print(f\"\\nWin Rate: {win_rate:.1f}%\")\n",
    "    print(f\"Winning Trades: {len(winning_trades)}\")\n",
    "    print(f\"Losing Trades: {len(losing_trades)}\")\n",
    "\n",
    "    # Calculate returns\n",
    "    total_pnl = positions[\"pnl_numeric\"].sum()\n",
    "    avg_pnl = positions[\"pnl_numeric\"].mean()\n",
    "    max_win = positions[\"pnl_numeric\"].max()\n",
    "    max_loss = positions[\"pnl_numeric\"].min()\n",
    "\n",
    "    print(f\"\\nTotal P&L: {total_pnl:.2f} USD\")\n",
    "    print(f\"Average P&L: {avg_pnl:.2f} USD\")\n",
    "    print(f\"Best Trade: {max_win:.2f} USD\")\n",
    "    print(f\"Worst Trade: {max_loss:.2f} USD\")\n",
    "\n",
    "    # Calculate risk metrics if we have both wins and losses\n",
    "    if len(winning_trades) > 0 and len(losing_trades) > 0:\n",
    "        avg_win = winning_trades[\"pnl_numeric\"].mean()\n",
    "        avg_loss = abs(losing_trades[\"pnl_numeric\"].mean())\n",
    "        profit_factor = winning_trades[\"pnl_numeric\"].sum() / abs(losing_trades[\"pnl_numeric\"].sum())\n",
    "\n",
    "        print(f\"\\nAverage Win: {avg_win:.2f} USD\")\n",
    "        print(f\"Average Loss: {avg_loss:.2f} USD\")\n",
    "        print(f\"Profit Factor: {profit_factor:.2f}\")\n",
    "        print(f\"Risk/Reward Ratio: {avg_win/avg_loss:.2f}\")\n",
    "else:\n",
    "    print(\"\\nNo positions generated. Check strategy parameters.\")\n",
    "\n",
    "print(\"\\n=== FINAL ACCOUNT STATE ===\")\n",
    "print(account.tail(1).to_string())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "32",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
